跳到主要内容

Web GUI开发 - React

阅读量: 101
阅读人次: 102

在很久以前,无意中接触到了 React,一直很喜欢它。工作中应该是用不到它了,但是一直想怎么去了解它。Web 前端开发中各种脚手架方便了入门使用,也屏蔽了太多东西。

多亏了现在 GPT 的强大以及 Google 良好的搜索能力,让我得以记录如何从0搭建一个 React Demo。

搭建项目

首先,需要安装 Node.js

然后,创建项目文件夹(这里我们以 ReactDemo 为例)。进入项目文件夹,然后执行 npm init,它会引导你输入一系列信息创建项目,并最终生成 package.json 文件:

{
"name": "reactdemo",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"description": ""
}

安装 webpack 相关依赖:

npm install webpack webpack-cli webpack-dev-server
  • webpack:构建工具
  • webpack-cli:命令行运行 webpack 的工具
  • webpack-dev-server:开发服务器

安装 webpack 依赖完成之后,package.json 会新增 dependencies 字段,记录依赖库及对应版本号:

{
// ......
"dependencies": {
"webpack": "^5.97.1",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.0"
}
// ......
}

安装 Babel 相关依赖:

npm install babel-loader @babel/preset-env @babel/core @babel/plugin-transform-runtime \
@babel/preset-react @babel/runtime @babel/cli
  • babel-loader:webpack 的加载器(loader),用于将 JavaScript 代码通过 Babel 进行转换。
  • @babel/preset-env:让我们可以使用最新的 JavaScript,而不用当心老版本的浏览器不兼容新ES标准。
  • @babel/core:Babel 的核心模块,它是 babel 编译器的主要部分。
  • @babel/plugin-transform-runtime:支持重复使用 Babel 注入的辅助代码以节省代码大小。
  • @babel/preset-react:Babel 的一个预设(preset),用于转换 React 中的 JSX 语法。
  • @babel/runtime:包含 polyfill 和许多其他 Babel 可以引用的包。
  • @babel/cli:支持通过命令行运行 babel

安装 react 和 react-dom:

npm install react react-dom

在项目根目录下创建 public 文件夹,然后在该文件夹下创建 index.html 文件,在该文件添加如下内容:

<body>
<div id="root"></div>
</body>
<script src="/main.js"></script>

在项目根目录下创建 src 文件夹,然后在该文件夹下创建 App.jsx 文件,在该文件添加如下内容:

import React from "react";

export default function App() {
return (
<h1>
从 Webpack 和 Babel 开始搭建 React 项目
</h1>
)
}

在项目根目录下(或其他位置)创建 index.jsx,它将作为 webpack 的入口点(Entry Point)。在该文件添加如下内容:

import React from "react";
import { createRoot } from 'react-dom/client';
import App from "./src/App.jsx"

const root = createRoot(document.getElementById('root'));
root.render(<App />);

在项目根目录下创建一个名为 webpack.config.js 的文件并添加以下代码。此文件包含负责将代码文件打包为单个文件,以及设置开发服务器的配置。

const path = require("path");

module.exports = {
mode: "development", // or "production",
entry: "./index.jsx",
output: {
path: path.resolve(__dirname, "public"),
filename: "main.js"
},
target: "web",
devServer: {
static: {
directory: path.join(__dirname, 'public'),
},
host: "127.0.0.1",
port: "9000",
open: true,
hot: true,
liveReload: true
},
resolve: {
extensions: ['.js', '.jsx', '.json']
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: ["@babel/preset-react"]
}
}
]
}
}

更新 package.json 文件:

{
// ...
"scripts": {
"start": "webpack-dev-server .",
"build": "webpack .",
"test": "echo \"Error: no test specified\" && exit 1"
},
// ...
}

安装 React Router

npm install react-router

使用 antd:

npm install antd

或使用安装 MUI

npm install @mui/material @emotion/react @emotion/styled

减小打包体积

通过代码分割(Code Splitting)减小文件体积

React 项目通常会构建成一个庞大的 JavaScript 包,特别是当项目规模较大时。代码分割可以将你的应用拆分成更小的、按需加载的 chunk。

使用 React.lazy()<Suspense/> 进行组件级代码分割:

import React, { lazy, Suspense } from "react";

const Demo = lazy(() => import("./Demo"))
export default function App() {
return (
<div>
<h1>
从 Webpack 和 Babel 开始搭建 React 项目
</h1>
<Suspense fallback="加载中...">
<Demo />
</Suspense>
</div>
)
}

上述方法,在页面路由下使用也非常有效,避免代码一次性加载的问题。

状态管理

useContextuseReducer 结合使用是一种强大的状态管理模式,类似于 React Redux,但更简单。

更新包

# 1. 检查可更新的包
npm outdated

# 2. 安全更新(遵循 package.json 中的版本范围)
npm update
提示

在日常开发过程中,最常使用的元素布局方式可能就是 Flexbox 了,不过在编码实现过程中,经常可能遇到父元素莫名奇妙被撑高,或者子元素的大小不是我们预所期的。我想主要原因可能就是CSS的元素布局是从经典的文档流模型一步一步发展过来的,其中有很多默认属性影响着新的布局方式。一不小心漏掉什么就会使我们抓瞎半天。

CSS很强大,常见的布局它肯定都能实现,如果我们的实现无法按照我们所预期的表现,应该沉下心来了解一下基础知识。

HTML 几个基本概念

视口(Viewport):浏览器窗口中实际显示网页内容的区域,不包括地址栏、书签栏等浏览器界面。可以通过 100vh100vw 单位引用其宽高。

文档流与盒模型:

HTML元素默认高度行为为 auto,及其高度由其内容决定:

/* 默认值 */
html {
height: auto; /* 由内容决定 */
}

body {
height: auto; /* 由内容决定 */
margin: 8px; /* 浏览器默认边距 */
}

在使用 Flexbox 布局时,我们一般希望父容器有明确的高度,所以一般推荐:

html, body {
height: 100%;
margin: 0;
}

CSS 元素布局可以分为一下几大类:正常流布局(Normal Flow)、浮动布局(Float)、定位布局(Positioning)、弹性盒子布局(Flexbox)、网格布局(Grid)。

其中正常流布局也即文档流布局,即像平常撰写的文档一样,块级元素垂直排列,独占一行;行内元素水平排列,直到占满容器宽度后换行。

CSS 测试模板代码

<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
html,
body,
#root {
height: 100%;
margin: 0;
}

#root {
display: flex;
flex-direction: column;
}

.item-a {
background-color: #88cce1;
}

.item-b {
flex-grow: 1;
background-color: #c68a9f;
display: flex;
flex-direction: row;
height: 0;
}

.item-c {
background-color: #80f29d;
}

.item-b-1 {
flex-grow: 1;
background-color: #7050e3;

}

.item-b-2 {
background-color: #b34040;
flex-grow: 10;
overflow: auto;
}

.content {
background-color: #cfd762;
height: 2000px;
}
</style>
</head>

<body>
<div id="root">
<div class="item-a">
Item A
</div>
<div class="item-b">
<div class="item-b-1">
Item B1
</div>
<div class="item-b-2">
<div class="content">
Content
</div>
</div>
</div>
<div class="item-c">
Item C
</div>
</div>
</body>

</html>

Flex Item 的默认最小尺寸

上述代码有一个问题,为什么 .item-b 必须设置 min-height: 0;.item-b-2overflow 属性才会生效。

这个问题涉及到 Flexbox 布局中的一个复杂但重要的概念:Flex Item 的默认最小尺寸 (Automatic Minimum Size)。

当一个 Flex 容器 (display: flex;) 设置了主轴方向(在这里是垂直方向,因为 #rootflex-direction: column;,而 .item-b#root 的 Flex Item)时,其子元素(Flex Items)有一个默认的最小尺寸限制。

  1. 默认值不是 0: 对于 Flex Item 来说,其主轴方向(在这里是高度)的默认最小尺寸属性是 min-height: auto,而 auto 在 Flexbox 布局中通常解析为一个基于内容的最小尺寸(min-content),而不是 0。
  2. 内容决定尺寸: 这意味着 Flex Item 默认不会收缩到小于其内容的最小尺寸,即使你给它设置了 flex-shrink: 1; 或使用了 flex-grow 试图让它占据剩余空间。它的最小高度会被它的内容(包括后代元素)撑开,以确保内容不会发生不必要的溢出。

在样板的代码中:

  1. #root 是一个垂直方向的 Flex 容器 (flex-direction: column;)。
  2. .item-b#root 中的一个 Flex Item,它被设置了 flex-grow: 1;,目的是占据 A 和 C 之间所有剩余的垂直空间。
  3. .item-b-2.item-b 的子元素(也是一个 Flex Item,因为 .item-bdisplay: flex;),它包含一个高度为 2000px.content 元素。item-b-2 设置了 overflow: auto;,目的是让它内部出现滚动条。

.item-b 没有设置 min-height: 0; 时:

  • .item-b 的默认最小高度 (min-height: auto) 被计算为其内容所需要的最小高度。
  • .item-b 的一个子元素是 .item-b-2,而 .item-b-2 内部有 2000px 高度的内容。
  • 因此,.item-b 的最小高度被其内容(也就是 .content2000px 高度)撑开,导致 .item-b 即使在 flex-grow 的计算中获得了剩余空间,其实际最小高度仍然是 2000px 左右。
  • 这样,.item-b 在垂直方向上会变得非常高,超出了视口,导致最外层的 body/html 出现滚动条,而 .item-b-2 自身的 overflow: auto; 则不会触发,因为它并没有被约束在一个更小的尺寸内。

.item-b 添加 min-height: 0;覆盖 Flex Item 默认的 min-height: auto 行为。

  • 它将 .item-b 的最小尺寸设置回 0
  • 这样,当 .item-b#root 中计算高度时,它将能够自由收缩(由于 flex-grow: 1; 的作用,它会占据剩余空间,但同时它有一个 0 的最小限制),使其高度被限制在 A 和 C 之间的可用空间内。
  • 一旦 .item-b 的高度被限制,它的子元素 .item-b-2 也就被限制在一个明确的(且小于内容需求的)高度内。
  • 此时,.item-b-2 内部的 2000px 内容就会超出 .item-b-2 的可用高度,从而触发 .item-b-2 上设置的 overflow: auto;,产生内部滚动条,而不是让整个页面溢出。

简而言之,min-height: 0; 解除了 Flex Item 的内容对主轴尺寸的最小限制,允许它收缩到足以让内部可滚动区域生效的尺寸。

HTML元素的高度计算

HTML 元素的高度计算并不是一个单一的流程,而是取决于其设置的 height 属性值 和其所处的 布局环境

总的来说,HTML 元素的高度计算是**自下而上(子元素到父元素)自上而下(父元素到子元素)**两种机制的结合,这取决于为元素指定的是 内容高度 还是 百分比/视口高度

自下而上 (子元素 → 父元素) - 默认/内容流

这是最常见、最基本的计算方式,也被称为收缩到合适(shrink-to-fit)或固有尺寸(Intrinsic Sizing)。机制:

当一个块级元素(如 <div><p>)的 height 属性设置为默认值 auto 时,其高度由其内容决定:

  • 子元素内容撑开父元素:浏览器会先计算所有子元素(文本、图片、嵌套的盒子)的高度,然后父元素的高度会收缩或扩展到刚好能包含所有子元素的高度。
  • 示例: 如果你有一个 <div>,里面放了三行文字,<div> 的高度就会是这三行文字所需高度的总和。

自上而下 (父元素 → 子元素) - 百分比/弹性布局

当元素的高度依赖于其父元素的尺寸时,就会发生自上而下的计算。机制:

  1. 百分比高度 (height: 50%):
    • 如果子元素的高度设置为百分比(例如 height: 50%),它必须先知道父元素确切的高度值,才能计算出自己的高度。
    • 关键限制: 如果父元素的高度本身也是 auto(即由内容决定),那么子元素的百分比高度就会失效(被视为 auto),除非父元素是 Flex 或 Grid 容器中的 Flex Item/Grid Item。
    • 因此,要让百分比高度生效,需要有一条从 htmlbody 开始(通常设置为 height: 100%height: 100vh)一直向下传递的明确高度链。
  2. 弹性布局 (Flexbox/Grid):
    • 在 Flex 或 Grid 容器中,子元素(Flex Items/Grid Items)的尺寸计算会更加复杂,它可能依赖于容器的主轴和交叉轴尺寸、flex-growflex-shrink 等属性。
    • 例如,如果父元素是垂直方向的 Flex 容器,子元素设置 flex-grow: 1;,那么父元素的高度需要先确定,然后子元素才能根据剩余空间来计算自己的高度。

结论:一个循环的计算模型

现代浏览器在渲染时,会通过一个复杂的渲染树布局算法来确定所有元素的最终尺寸,这个过程实际上是这两者的结合:

  1. 浏览器首先尝试通过内容来确定元素的固有尺寸(自下而上)。
  2. 然后,它检查是否有百分比或 Flexbox/Grid 规则,这要求它知道包含块的尺寸(自上而下)。
  3. 如果两者发生冲突或相互依赖,浏览器会遵循 CSS 规范中的复杂规则来解析,有时需要设置如 min-height: 0 这样的属性来解除自下而上的最小尺寸限制,从而让自上而下的空间分配规则能够生效。